
#include <stdio.h>
#include <string.h>

#include "land_graphics.h"

#define LAND_OGL_CORE

#include "init.h"

static const char VERT_HEADER[] = "\n"
"#version 330 core\n"
"layout (location = 0) in vec4 landPosition;\n"
"layout (location = 1) in vec4 landUv;\n"
"layout (location = 2) in vec4 landColor;\n"
"layout (location = 3) in vec4 landNormal;\n"
"uniform mat4 landTransform = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
"uniform mat4 landView = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
"uniform mat4 landProjection = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
;

static const char FRAG_HEADER[] = "\n"
"#version 330 core\n"
;

#define ERR_LEN 1024
static struct {
	char err[ERR_LEN];
	LandShader* curBind;
	const char* source[LAND_MAX_SHADER_SOURCE];
	GLint lengths[LAND_MAX_SHADER_SOURCE];
	LandDepthTest depthTest;
}ctx = {0};


static void
bindDephtTest(LandDepthTest depthTest) {

	//glDisable(GL_DEPTH_TEST);
	//glEnable(GL_DEPTH_TEST);
	//glDepthFunc(GL_ALWAYS);
	return;
	//if (ctx.depthTest == depthTest) {
	//	return;
	//}
	ctx.depthTest = depthTest;
	switch (depthTest) {
	case LAND_DEPTH_TEST_ALWAYS:
		glDepthFunc(GL_ALWAYS);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_NEVER:
		glDepthFunc(GL_NEVER);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_LESS:
		glDepthFunc(GL_LESS);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_GREATER:
		glDepthFunc(GL_GREATER);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_NEQUAL:
		glDepthFunc(GL_NOTEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_LEQUAL:
		glDepthFunc(GL_LEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	case LAND_DEPTH_TEST_GEQUAL:
		glDepthFunc(GL_GEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	default:
		glDisable(GL_DEPTH_TEST);
	}
}

static void
setUniformF(GLint pos, LandFloat f) {
	glUniform1f(pos, (GLfloat)f);
}

static void
setUniformV(GLint pos, LandVector* v) {
	glUniform4f(pos, (GLfloat)v->x,
		(GLfloat)v->y, (GLfloat)v->z, (GLfloat)v->w);
}

static void
setUniformM(GLint pos, LandMatrix* mat) {
	GLfloat t[16];
	for (int i = 0; i < 16; i++) {
		t[i] = (GLfloat)mat->B[i];
	}
	glUniformMatrix4fv(pos, 1, GL_FALSE, t);
}

static void
uploadUniforms(LandOglShader* shader, LandDraw* draw) {
	bindDephtTest(draw->depthTest);
	GLint pos;
	LandUniform* u;
	for (unsigned i = 0; i < draw->uniformCount; i++) {
		u = &draw->uniforms[i];
		pos = glGetUniformLocation(shader->program, u->name);
		if (pos < 0) {
			continue;
		}
		switch (u->type) {
		case LAND_GRAPHICS_TFLOAT:
			setUniformF(pos, u->f);
			break;
		case LAND_GRAPHICS_TVECTOR:
			setUniformV(pos, &u->v);
			break;
		case LAND_GRAPHICS_TMATRIX:
			setUniformM(pos, &u->m);
			break;
		case LAND_GRAPHICS_TTEXTURE:
			glActiveTexture(GL_TEXTURE0 + pos);
			glBindTexture(GL_TEXTURE_2D, landGetOglTextureId(u->tex));
			GLint err = glGetError();
			if (err) {
				printf("LAND_GRAPHICS_TTEXTURE err:%d\n", err);
			}
			break;
		case LAND_GRAPHICS_TCANVAS:
			glActiveTexture(GL_TEXTURE0 + pos);
			glBindTexture(GL_TEXTURE_2D, landGetOglCanvasTexId(u->canvas));
			break;
		}
	}
}

static size_t
sizeofShader(){
	return sizeof(LandOglShader);
}

static inline LandOglShader*
useShader(LandShader* shader) {
	if (ctx.curBind == shader) {
		return (LandOglShader*)shader;
	}
	LandOglShader* oglShader = (LandOglShader*)shader;
	if (!oglShader->program) {
		return NULL;
	}
	ctx.curBind = shader;
	glUseProgram(oglShader->program);
	return oglShader;
}

static int
newShader(LandShader* shader) {
	LandOglShader* oglShader = (LandOglShader*)shader;
	oglShader->program = 0;
	return 0;
}
static void
freeShader(LandShader* shader) {
	LandOglShader* oglShader = (LandOglShader*)shader;
	if (oglShader->program) {
		glDeleteProgram(oglShader->program);
	}
	if (ctx.curBind == shader){
		ctx.curBind = NULL;
		glUseProgram(0);
	}
}

static int
compileShader(LandShader* shader, const char** err) {
	LandOglShader* oglShader = (LandOglShader*)shader;
	GLint ret;
	const char** source = ctx.source;
	GLint* lengths = ctx.lengths;
	source[0] = VERT_HEADER;
	lengths[0] = (GLint)strlen(VERT_HEADER);
	for(unsigned i = 0; i < shader->vsCount; i ++){
		source[i + 1] = shader->vs[i].data;
		lengths[i + 1] = (GLint)shader->vs[i].size;
	}
	GLuint vsId= glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vsId, shader->vsCount + 1, source, lengths);
	glCompileShader(vsId);
	glGetShaderiv(vsId, GL_COMPILE_STATUS, &ret);
	if (!ret) {
		glGetShaderInfoLog(vsId, ERR_LEN, NULL, ctx.err);
		glDeleteShader(vsId);
		*err = ctx.err;
		return 1;
	}
	source[0] = FRAG_HEADER;
	lengths[0] = (GLint)strlen(FRAG_HEADER);
	for (unsigned i = 0; i < shader->fsCount; i++) {
		source[i + 1] = shader->fs[i].data;
		lengths[i + 1] = (GLint)shader->fs[i].size;
	}
	GLuint fsId = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fsId, shader->fsCount + 1, source, lengths);
	glCompileShader(fsId);
	glGetShaderiv(fsId, GL_COMPILE_STATUS, &ret);
	if (!ret) {
		glGetShaderInfoLog(fsId, ERR_LEN, NULL, ctx.err);
		glDeleteShader(fsId);
		glDeleteShader(vsId);
		*err = ctx.err;
		return 1;
	}
	GLuint program = glCreateProgram();
	glAttachShader(program, vsId);
	glAttachShader(program, fsId);
	glLinkProgram(program);
	glGetProgramiv(program, GL_LINK_STATUS, &ret);
	glDeleteShader(vsId);
	glDeleteShader(fsId);
	if (!ret) {
		glGetProgramInfoLog(oglShader->program, ERR_LEN, NULL, ctx.err);
		*err = ctx.err;
		glDeleteProgram(oglShader->program);
		return 1;
	}
	oglShader->program = program;
	oglShader->projPos = glGetUniformLocation(program, "landProjection");
	oglShader->viewPos = glGetUniformLocation(program, "landView");
	oglShader->transformPos = glGetUniformLocation(program, "landTransform");
	glUseProgram(program);
	return 0;
}


static LandShaderBackend backend = {
	sizeofShader,
	newShader,
	freeShader,
	compileShader,
};

LandShaderBackend*
landGetOglShaderBackend() {
	return &backend;
}


void
landBindOglShader(LandCanvas* canvas, LandDraw* draw) {
	LandShader* shader = draw->shader;
	LandOglShader* oglShader = useShader(shader);
	if (oglShader->projPos >= 0 && draw->projection) {
		setUniformM(oglShader->projPos, draw->projection);
	}
	if (oglShader->viewPos >= 0 && draw->view) {
		setUniformM(oglShader->viewPos, draw->view);
	}
	if (oglShader->transformPos >= 0 && draw->transform) {
		setUniformM(oglShader->transformPos, draw->transform);
	}
	uploadUniforms(oglShader, draw);
}

